<!doctype html>
<head>
    <title>Threebox WASD driving game</title>
    <script src="https://api.mapbox.com/mapbox-gl-js/v1.11.1/mapbox-gl.js"></script>
    <link href="https://api.mapbox.com/mapbox-gl-js/v1.11.1/mapbox-gl.css" rel="stylesheet" />
    <script src="../dist/threebox.js" type="text/javascript"></script>
    <link href="../dist/threebox.css" rel="stylesheet" />
    <script src="config.js"></script>
    <style>
        body, html {
            width: 100%;
            height: 100%;
            margin: 0;
        }

        #map {
            width: 100%;
            height: 100%;
        }

		.helpDiv {
			width: auto;
			left: 50%;
			top: 0px;
			z-index: 2;
			position: absolute;
		}

		.help {
			background: rgba(0, 0, 0, 0.5);
			color: #fff;
			position: relative;
			text-align: center;
			top: 10px;
			left: -50%;
			padding: 5px 10px;
			margin: 0;
			font-size: 11px;
			line-height: 18px;
			border-radius: 3px;
			z-index: 1;
			display: block;
		}
    </style>

</head>
<body>
	<div id='map' class='map'>
		<div class="mapboxgl-ctrl helpDiv">
			<div id="help" class="help">
				Press "W", "A", "S", "D" keys to drive the truck
			</div>
		</div>
	</div>

	<script type="module">
		if (!config) console.error("Config not set! Make a copy of 'config_template.js', add in your access token, and save the file as 'config.js'.");

		mapboxgl.accessToken = config.accessToken;

		let minZoom = 12;
		let mapConfig = {
			map: { center: [-122.4301905, 37.7298202], zoom: 20, pitch: 60, bearing: 38 }, 
			truck: { origin: [-122.4301905, 37.7298202, 0], type: 'glb', model: './models/vehicles/car', rotation: { x: 90, y: -90, z: 0 }, scale: 5, startRotation: { x: 0, y: 0, z: -38 }, date: new Date(2020, 6, 19, 23) },
			names: {
				compositeSource: "composite",
				compositeSourceLayer: "building",
				compositeLayer: "3d-buildings"
			}
		}

		let map = new mapboxgl.Map({
			container: 'map',
			style: 'mapbox://styles/mapbox/satellite-streets-v11',
			zoom: mapConfig.map.zoom,
			center: mapConfig.map.center,
			pitch: mapConfig.map.pitch,
			bearing: mapConfig.map.bearing,
			antialias: true // create the gl context with MSAA antialiasing, so custom layers are antialiased
		});

		window.tb = new Threebox(
			map,
			map.getCanvas().getContext('webgl'),
			{
				realSunlight: true,
				enableSelectingObjects: true,
				enableDraggingObjects: true,
				enableRotatingObjects: true,
				enableTooltips: true
			}
		);

		tb.setSunlight(new Date(2020, 6, 19, 23), map.getCenter());

		// parameters to ensure the model is georeferenced correctly on the map
		let truck;

		function createCustomLayer(layerName) {
			let model;
			//create the layer
			let customLayer3D = {
				id: layerName,
				type: 'custom',
				renderingMode: '3d',
				onAdd: function (map, gl) {

					let options = {
						type: mapConfig.truck.type, //model type
						obj: mapConfig.truck.model + "." + mapConfig.truck.type, 
						units: 'meters', // in meters
						scale: mapConfig.truck.scale, //x3 times is real size for this model
						rotation: mapConfig.truck.rotation, //default rotation
						anchor: 'top'
					}
					tb.loadObj(options, function (model) {
						truck = model.setCoords(mapConfig.truck.origin);
						truck.setRotation(mapConfig.truck.startRotation); //turn it to the initial street way
						truck.addTooltip("Drive with WASD keys", true, truck.anchor, true, 2);
						truck.castShadow = true;
						truck.selected = true;
						truck.addEventListener('ObjectChanged', onObjectChanged, false);

						tb.add(truck);
						init();

					});


				},
				render: function (gl, matrix) {
					tb.update();
				}
			};
			return customLayer3D;

		};

		function easing(t) {
			return t * (2 - t);
		}

		let velocity = 0.0, speed = 0.0, ds = 0.01;
		let keys;

		map.on('style.load', function () {

			map.addLayer(createCustomLayer('3d-model'));

			let l = mapConfig.names.compositeLayer;
			if (api.buildings) {
				if (!map.getLayer(l)) { map.addLayer(createCompositeLayer(l)); }
			}
			map.getCanvas().focus();

		});

		function createCompositeLayer(layerId) {
			let layer = {
				'id': layerId,
				'source': mapConfig.names.compositeSource,
				'source-layer': mapConfig.names.compositeSourceLayer,
				'filter': ['==', 'extrude', 'true'],
				'type': 'fill-extrusion',
				'minzoom': minZoom,
				'paint': {
					'fill-extrusion-color':
						[
							'case',
							['boolean', ['feature-state', 'select'], false],
							"red",
							['boolean', ['feature-state', 'hover'], false],
							"lightblue",
							'#aaa'
						],

					// use an 'interpolate' expression to add a smooth transition effect to the
					// buildings as the user zooms in
					'fill-extrusion-height': [
						'interpolate',
						['linear'],
						['zoom'],
						minZoom,
						0,
						minZoom + 0.05,
						['get', 'height']
					],
					'fill-extrusion-base': [
						'interpolate',
						['linear'],
						['zoom'],
						minZoom,
						0,
						minZoom + 0.05,
						['get', 'min_height']
					],
					'fill-extrusion-opacity': 0.9
				}
			};
			return layer;
		}

		import { GUI } from 'https://threejs.org/examples/jsm/libs/dat.gui.module.js';
		import Stats from 'https://threejs.org/examples/jsm/libs/stats.module.js';
		let stats, gui;
		let fHover;

		let api = {
			buildings: true,
			acceleration: 5, 
			inertia: 3
		};

		function init() {
			// stats
			stats = new Stats();
			map.getContainer().appendChild(stats.dom);

			keys = {
				a: false,
				s: false,
				d: false,
				w: false
			};

			document.body.addEventListener('keydown', function (e) {

				const key = e.code.replace('Key', '').toLowerCase();
				if (keys[key] !== undefined)
					keys[key] = true;
			});
			document.body.addEventListener('keyup', function (e) {

				const key = e.code.replace('Key', '').toLowerCase();
				if (keys[key] !== undefined)
					keys[key] = false;
			});

			animate();

			// gui
			gui = new GUI();
			// this will define if there's a fixed zoom level for the model
			gui.add(api, 'buildings').name('buildings').onChange(changeGui);
			gui.add(api, 'acceleration', 1, 10).step(0.5);
			gui.add(api, 'inertia', 1, 5).step(0.5);
		}

		function changeGui() {
			let l = mapConfig.names.compositeLayer;
			if (api.buildings) {
				if (!map.getLayer(l)) { map.addLayer(createCompositeLayer(l)); }
			}
			else {
				if (map.getLayer(l)) { map.removeLayer(l)}

			}

			tb.map.repaint = true;
		}

		function animate() {

			requestAnimationFrame(animate);
			stats.update();
			speed = 0.0;

			if (!(keys.w || keys.s)) {
				if (velocity > 0) { speed = -api.inertia * ds }
				else if (velocity < 0) { speed = api.inertia * ds }
				if (velocity > -0.0008 && velocity < 0.0008) { speed = velocity = 0.0; return; }
			} 

			if (keys.w)
				speed = api.acceleration * ds;
			else if (keys.s)
				speed = -api.acceleration * ds;

			velocity += (speed - velocity) * api.acceleration * ds;
			if (speed == 0.0) {
				velocity = 0;
				return;
			}

			truck.set({ worldTranslate: new THREE.Vector3(0, -velocity, 0) });

			let options = {
				center: truck.coordinates,
				bearing: map.getBearing(),
				easing: easing
			};

			function toDeg(rad) {
				return rad / Math.PI * 180;
			}

			function toRad(deg) {
				return deg * Math.PI / 180;
			}

			let deg = 1;
			let rad = toRad(deg);
			let zAxis = new THREE.Vector3(0, 0, 1);

			if (keys.a || keys.d) {
				rad *= (keys.d ? -1 : 1);
				truck.set({ quaternion: [zAxis, truck.rotation.z + rad] });
				options.bearing = -toDeg(truck.rotation.z);
			}

			map.jumpTo(options);
			tb.map.update = true;

		}

		function onObjectChanged(e) {
			let model = e.detail.object; //here's the object already modified
			if (api.buildings) {
				let c = model.coordinates;
				let point = map.project(c);
				let features = map.queryRenderedFeatures(point, { layers: [mapConfig.names.compositeLayer] });
				if (features.length > 0) {
					light(features[0]); // crash!
				}
			}
		}

		function light(feature) {
			fHover = feature;
			map.setFeatureState({
				source: fHover.source,
				sourceLayer: fHover.sourceLayer,
				id: fHover.id
			}, { select: true });
		}


	</script>
</body>
